import { execScripts } from 'import-html-entry';
import { isFunction, noop } from 'lodash';
import { checkActivityFunctions } from 'single-spa';
import { frameworkConfiguration } from '../../apis';
import { Freer } from '../../interfaces';
import { attachDocProxySymbol } from '../common';
import * as css from './css';
const styledComponentSymbol = Symbol('styled-component-qiankun');
const attachElementContainerSymbol = Symbol('attach-proxy-container');
const rawHeadAppendChild = HTMLHeadElement.prototype.appendChild;
const rawHeadRemoveChild = HTMLHeadElement.prototype.removeChild;
const rawBodyAppendChild = HTMLBodyElement.prototype.appendChild;
const rawBodyRemoveChild = HTMLBodyElement.prototype.removeChild;
const rawHeadInsertBefore = HTMLHeadElement.prototype.insertBefore;
const rawRemoveChild = HTMLElement.prototype.removeChild;
const rawDocumentCreateElement = Document.prototype.createElement;
const SCRIPT_TAG_NAME = 'SCRIPT';
const LINK_TAG_NAME = 'LINK';
const STYLE_TAG_NAME = 'STYLE';
const proxyContainerInfoMapper = new Map();

function isHijackingTag(tagName) {
  return tagName?.toUpperCase() === LINK_TAG_NAME || tagName?.toUpperCase() === STYLE_TAG_NAME || tagName?.toUpperCase() === SCRIPT_TAG_NAME;
}

function isStyledComponentsLike(element) {
  return !element.textContent && (element.sheet?.cssRules.length || getCachedRules(element)?.length);
}

function getCachedRules(element) {
  return element[styledComponentSymbol];
}

function setCachedRules(element, cssRules) {
  Object.defineProperty(element, styledComponentSymbol, {
    value: cssRules,
    configurable: true,
    enumerable: false
  });
}

function patchCustomEvent(e, elementGetter) {
  Object.defineProperties(e, {
    srcElement: {
      get: elementGetter
    },
    target: {
      get: elementGetter
    }
  });
  return e;
}

function getOverwrittenAppendChildOrInsertBefore(opts) {
  return function appendChildOrInsertBefore(newChild, refChild) {
    let element = newChild;
    const {
      rawDOMAppendOrInsertBefore
    } = opts;

    if (element.tagName) {
      let {
        appName,
        appWrapperGetter,
        proxy,
        singular,
        dynamicStyleSheetElements
      } = opts;
      const {
        scopedCSS,
        excludeAssetFilter
      } = opts;
      const storedContainerInfo = element[attachElementContainerSymbol];

      if (storedContainerInfo) {
        appName = storedContainerInfo.appName;
        singular = storedContainerInfo.singular;
        appWrapperGetter = storedContainerInfo.appWrapperGetter;
        dynamicStyleSheetElements = storedContainerInfo.dynamicStyleSheetElements;
        proxy = storedContainerInfo.proxy;
      }

      const invokedByMicroApp = singular ? checkActivityFunctions(window.location).some(name => name === appName) : !!storedContainerInfo;

      switch (element.tagName) {
        case LINK_TAG_NAME:
        case STYLE_TAG_NAME:
          {
            const stylesheetElement = newChild;
            const {
              href
            } = stylesheetElement;

            if (!invokedByMicroApp || excludeAssetFilter && href && excludeAssetFilter(href)) {
              return rawDOMAppendOrInsertBefore.call(this, element, refChild);
            }

            const mountDOM = appWrapperGetter();

            if (scopedCSS) {
              css.process(mountDOM, stylesheetElement, appName);
            }

            dynamicStyleSheetElements.push(stylesheetElement);
            const referenceNode = mountDOM.contains(refChild) ? refChild : null;
            return rawDOMAppendOrInsertBefore.call(mountDOM, stylesheetElement, referenceNode);
          }

        case SCRIPT_TAG_NAME:
          {
            const {
              src,
              text
            } = element;

            if (!invokedByMicroApp || excludeAssetFilter && src && excludeAssetFilter(src)) {
              return rawDOMAppendOrInsertBefore.call(this, element, refChild);
            }

            const mountDOM = appWrapperGetter();
            const {
              fetch
            } = frameworkConfiguration;
            const referenceNode = mountDOM.contains(refChild) ? refChild : null;

            if (src) {
              execScripts(null, [src], proxy, {
                fetch,
                strictGlobal: !singular,
                beforeExec: () => {
                  Object.defineProperty(document, 'currentScript', {
                    get() {
                      return element;
                    },

                    configurable: true
                  });
                },
                success: () => {
                  const loadEvent = new CustomEvent('load');

                  if (isFunction(element.onload)) {
                    element.onload(patchCustomEvent(loadEvent, () => element));
                  } else {
                    element.dispatchEvent(loadEvent);
                  }

                  element = null;
                },
                error: () => {
                  const errorEvent = new CustomEvent('error');

                  if (isFunction(element.onerror)) {
                    element.onerror(patchCustomEvent(errorEvent, () => element));
                  } else {
                    element.dispatchEvent(errorEvent);
                  }

                  element = null;
                }
              });
              const dynamicScriptCommentElement = document.createComment(`dynamic script ${src} replaced by qiankun`);
              return rawDOMAppendOrInsertBefore.call(mountDOM, dynamicScriptCommentElement, referenceNode);
            }

            execScripts(null, [`<script>${text}</script>`], proxy, {
              strictGlobal: !singular,
              success: element.onload,
              error: element.onerror
            });
            const dynamicInlineScriptCommentElement = document.createComment('dynamic inline script replaced by qiankun');
            return rawDOMAppendOrInsertBefore.call(mountDOM, dynamicInlineScriptCommentElement, referenceNode);
          }

        default:
          break;
      }
    }

    return rawDOMAppendOrInsertBefore.call(this, element, refChild);
  };
}

function getNewRemoveChild(opts) {
  return function removeChild(child) {
    const {
      headOrBodyRemoveChild
    } = opts;

    try {
      const {
        tagName
      } = child;

      if (isHijackingTag(tagName)) {
        let {
          appWrapperGetter
        } = opts;
        const storedContainerInfo = child[attachElementContainerSymbol];

        if (storedContainerInfo) {
          appWrapperGetter = storedContainerInfo.appWrapperGetter;
        }

        const container = appWrapperGetter();

        if (container.contains(child)) {
          return rawRemoveChild.call(container, child);
        }
      }
    } catch (e) {
      console.warn(e);
    }

    return headOrBodyRemoveChild.call(this, child);
  };
}

function patchHTMLDynamicAppendPrototypeFunctions(appName, appWrapperGetter, proxy, singular = true, scopedCSS = false, dynamicStyleSheetElements, excludeAssetFilter) {
  if (HTMLHeadElement.prototype.appendChild === rawHeadAppendChild && HTMLBodyElement.prototype.appendChild === rawBodyAppendChild && HTMLHeadElement.prototype.insertBefore === rawHeadInsertBefore) {
    HTMLHeadElement.prototype.appendChild = getOverwrittenAppendChildOrInsertBefore({
      rawDOMAppendOrInsertBefore: rawHeadAppendChild,
      appName,
      appWrapperGetter,
      proxy,
      singular,
      dynamicStyleSheetElements,
      scopedCSS,
      excludeAssetFilter
    });
    HTMLBodyElement.prototype.appendChild = getOverwrittenAppendChildOrInsertBefore({
      rawDOMAppendOrInsertBefore: rawBodyAppendChild,
      appName,
      appWrapperGetter,
      proxy,
      singular,
      dynamicStyleSheetElements,
      scopedCSS,
      excludeAssetFilter
    });
    HTMLHeadElement.prototype.insertBefore = getOverwrittenAppendChildOrInsertBefore({
      rawDOMAppendOrInsertBefore: rawHeadInsertBefore,
      appName,
      appWrapperGetter,
      proxy,
      singular,
      dynamicStyleSheetElements,
      scopedCSS,
      excludeAssetFilter
    });
  }

  if (HTMLHeadElement.prototype.removeChild === rawHeadRemoveChild && HTMLBodyElement.prototype.removeChild === rawBodyRemoveChild) {
    HTMLHeadElement.prototype.removeChild = getNewRemoveChild({
      appWrapperGetter,
      headOrBodyRemoveChild: rawHeadRemoveChild
    });
    HTMLBodyElement.prototype.removeChild = getNewRemoveChild({
      appWrapperGetter,
      headOrBodyRemoveChild: rawBodyRemoveChild
    });
  }

  return function unpatch(recoverPrototype) {
    if (recoverPrototype) {
      HTMLHeadElement.prototype.appendChild = rawHeadAppendChild;
      HTMLHeadElement.prototype.removeChild = rawHeadRemoveChild;
      HTMLBodyElement.prototype.appendChild = rawBodyAppendChild;
      HTMLBodyElement.prototype.removeChild = rawBodyRemoveChild;
      HTMLHeadElement.prototype.insertBefore = rawHeadInsertBefore;
    }
  };
}

function patchDocumentCreateElement(appName, appWrapperGetter, singular, proxy, dynamicStyleSheetElements) {
  if (singular) {
    return noop;
  }

  proxyContainerInfoMapper.set(proxy, {
    appName,
    proxy,
    appWrapperGetter,
    dynamicStyleSheetElements,
    singular
  });

  if (Document.prototype.createElement === rawDocumentCreateElement) {
    Document.prototype.createElement = function createElement(tagName, options) {
      const element = rawDocumentCreateElement.call(this, tagName, options);

      if (isHijackingTag(tagName)) {
        const proxyContainerInfo = proxyContainerInfoMapper.get(this[attachDocProxySymbol]);

        if (proxyContainerInfo) {
          Object.defineProperty(element, attachElementContainerSymbol, {
            value: proxyContainerInfo,
            enumerable: false
          });
        }
      }

      return element;
    };
  }

  return function unpatch(recoverPrototype) {
    proxyContainerInfoMapper.delete(proxy);

    if (recoverPrototype) {
      Document.prototype.createElement = rawDocumentCreateElement;
    }
  };
}

let bootstrappingPatchCount = 0;
let mountingPatchCount = 0;
export default function patch(appName, appWrapperGetter, proxy, mounting = true, singular = true, scopedCSS = false, excludeAssetFilter) {
  let dynamicStyleSheetElements = [];
  const unpatchDocumentCreate = patchDocumentCreateElement(appName, appWrapperGetter, singular, proxy, dynamicStyleSheetElements);
  const unpatchDynamicAppendPrototypeFunctions = patchHTMLDynamicAppendPrototypeFunctions(appName, appWrapperGetter, proxy, singular, scopedCSS, dynamicStyleSheetElements, excludeAssetFilter);
  if (!mounting) bootstrappingPatchCount++;
  if (mounting) mountingPatchCount++;
  return function free() {
    if (!mounting && bootstrappingPatchCount !== 0) bootstrappingPatchCount--;
    if (mounting) mountingPatchCount--;
    const allMicroAppUnmounted = mountingPatchCount === 0 && bootstrappingPatchCount === 0;
    unpatchDynamicAppendPrototypeFunctions(allMicroAppUnmounted);
    unpatchDocumentCreate(allMicroAppUnmounted);
    dynamicStyleSheetElements.forEach(stylesheetElement => {
      if (stylesheetElement instanceof HTMLStyleElement && isStyledComponentsLike(stylesheetElement)) {
        if (stylesheetElement.sheet) {
          setCachedRules(stylesheetElement, stylesheetElement.sheet.cssRules);
        }
      }
    });
    return function rebuild() {
      dynamicStyleSheetElements.forEach(stylesheetElement => {
        document.head.appendChild.call(appWrapperGetter(), stylesheetElement);

        if (stylesheetElement instanceof HTMLStyleElement && isStyledComponentsLike(stylesheetElement)) {
          const cssRules = getCachedRules(stylesheetElement);

          if (cssRules) {
            for (let i = 0; i < cssRules.length; i++) {
              const cssRule = cssRules[i];
              stylesheetElement.sheet.insertRule(cssRule.cssText);
            }
          }
        }
      });

      if (mounting) {
        dynamicStyleSheetElements = [];
      }
    };
  };
}